We previously covered several limitations of Terragrunt managing the creation of the state bucket, log bucket, and lock table used for storing Terraform remote state in AWS. For instance, we often want full control over encryption properties, and Terragrunt does not allow you to specify things like custom encryption keys.
These limitations often drive our team to eschew Terragrunt’s management of the operational infrastructure Terraform requires. Many of our teams have found CloudFormation to be an attractive alternative for managing this operational infrastructure.
Chris, BTI360 engineer and author of thirstydeveloper.io, will show us how to get started using CloudFormation to bootstrap an account for Terraform and Terragrunt.
Terraform State with CloudFormation
We’ll start with a CloudFormation template that exactly mirrors what Terragrunt creates for you so that:
- You can use this template to import resources Terragrunt has already created for you
- We can discuss ways to harden beyond what Terragrunt does for you
Assuming you have Terragrunt’s remote_state
configured as follows:
locals { root_deployments_dir = get_parent_terragrunt_dir() relative_deployment_path = path_relative_to_include() deployment_path_components = compact(split("/", local.relative_deployment_path)) tier = local.deployment_path_components[0] stack = reverse(local.deployment_path_components)[0] } remote_state { backend = "s3" generate = { path = "backend.tf" if_exists = "overwrite" } config = { bucket = "bti360-terraform-state" region = "us-east-1" encrypt = true key = "${dirname(local.relative_deployment_path)}/${local.stack}.tfstate" dynamodb_table = "bti360-terraform-state-locks" accesslogging_bucket_name = "bti360-terraform-state-logs" } }
and assuming you’re using Terragrunt 0.26.4, here’s a CloudFormation template that matches what Terragrunt creates with the above configuration:
--- AWSTemplateFormatVersion: '2010-09-09' Description: Deploy terraform operational infrastructure Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Terraform State Resources Parameters: - StateBucketName - StateLogBucketName - LockTableName Parameters: StateBucketName: Type: String Description: Name of the S3 bucket for terraform state StateLogBucketName: Type: String Description: Name of the S3 bucket for terraform state logs LockTableName: Type: String Description: Name of the terraform DynamoDB lock table Resources: TerraformStateLogBucket: Type: 'AWS::S3::Bucket' DeletionPolicy: Retain UpdateReplacePolicy: Retain Properties: BucketName: !Ref StateLogBucketName AccessControl: LogDeliveryWrite TerraformStateBucket: Type: 'AWS::S3::Bucket' DeletionPolicy: Retain UpdateReplacePolicy: Retain Properties: BucketName: !Ref StateBucketName BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: aws:kms LoggingConfiguration: DestinationBucketName: !Ref StateLogBucketName LogFilePrefix: TFStateLogs/ PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True VersioningConfiguration: Status: Enabled TerraformStateLockTable: Type: 'AWS::DynamoDB::Table' DeletionPolicy: Retain UpdateReplacePolicy: Retain Properties: TableName: !Ref LockTableName AttributeDefinitions: - AttributeName: LockID AttributeType: S KeySchema: - AttributeName: LockID KeyType: HASH BillingMode: PAY_PER_REQUEST
Put this YAML into a terraform-bootstrap.cf.yml
file. For today’s post, we’re going to assume you’re working from a clean slate. If you already have a state bucket, log bucket, or lock table that Terragrunt created for you, you can use CloudFormation’s resource import feature to bring those under CloudFormation’s control. thirstydeveloper.io has a step-by-step guide on importing Terragrunt created resources into CloudFormation.
Assuming you’re starting fresh, run the following to deploy your CloudFormation stack:
aws --profile bti360 cloudformation deploy \ --template-file terraform-bootstrap.cf.yml --stack-name terraform-bootstrap \ --capabilities CAPABILITY_NAMED_IAM \ --parameter-overrides \ StateBucketName=bti360-terraform-state \ StateLogBucketName=bti360-terraform-state-logs \ LockTableName=bti360-terraform-state-locks
Once the stack finishes deployment, you can run terragrunt
commands with the above remote_state
configuration, and Terragrunt will use the state bucket, log bucket, and lock table CloudFormation created.
Hardening Opportunities
With Terraform’s operational infrastructure managed by CloudFormation, let’s discuss ways to improve Terragrunt’s defaults.
Here are four specific hardening opportunities our teams often exercise:
1. Encryption on the logs bucket
Terragrunt sadly does not enable encryption on the logs bucket by default. We can remedy this in CloudFormation by specifying the BucketEncryption property, just like we do on the state bucket.
2. Block public access on the logs bucket
Similarly, the logs bucket does not explicitly block public access. We can do that with a PublicAccessBlockConfiguration property, again mirroring our state bucket.
3. Encrypt the lock table
We can enable encryption on the DynamoDB lock table using the SSESpecification property.
4. Restrict access to the state bucket
We can use an AWS::S3::BucketPolicy resource to lock down which IAM principals can access our Terraform state. Implementing access control around Terraform state is an excellent idea considering (1) the state almost always contains sensitive information, and (2) losing state can cause tremendous pain to import existing resources back under Terraform’s control.
Conclusion
While we use Terraform for the vast majority of our infrastructure-as-code needs at BTI360, we’ve found CloudFormation quite useful for bootstrapping an AWS account for Terraform management. We heartily recommend CloudFormation if your team has bumped into the limitations of letting Terragrunt manage the creation of Terraform’s operational infrastructure. For more information on this approach, including a fully worked example, see part 5 of Chris’ terraform-skeleton series on thirstydeveloper.io.
Interested in Solving Challenging Problems? Work Here!
Are you a software engineer, interested in joining a software company that invests in its teammates and promotes a strong engineering culture? Then you’re in the right place! Check out our current Career Opportunities. We’re always looking for like-minded engineers to join the BTI360 family.